/**
 * 
 */
package com.gragra.sampling.binary;
import com.gragra.sampling.PopulationProbabilityAssigner;
import com.gragra.sampling.ThreadedUniformAccess;
import com.gragra.sampling.sequentialStructure.WordAndTagHolder;
/**
 * @author Christoph Teichmann
 * 15.09.2013
 */
public class BinaryGibbs extends BinaryStructure
{
	/**
	 * @param wth
	 * @param threads
	 * @param cs
	 * @param sampleSpilt
	 */
	public BinaryGibbs(WordAndTagHolder wth, int threads,
			ConvertSample cs, boolean sampleSplit)
	{
		super(wth, threads, cs,sampleSplit);
		if(sampleSplit)
		{this.probs = new double[8];}
		else
		{this.probs = new double[2];}
	}
	@Override
	protected void coreSampling(double annealing, int thread,
			ThreadedUniformAccess tua, PopulationProbabilityAssigner ba)
	{
		for(int splitPoint=1;splitPoint<this.getLength();++splitPoint)
		{sampleSplitPoint(splitPoint,ba,annealing,thread,tua);}
	}
	/**
	 * 
	 * @param splitPoint
	 * @param ba
	 * @param annealing
	 * @param thread
	 * @param tua
	 */
	private void sampleSplitPoint(int splitPoint, PopulationProbabilityAssigner ba,
			double annealing, int thread, ThreadedUniformAccess tua)
	{
		for(int minPos=1;minPos<this.getLength();++minPos)
		{
			int node = this.getNodeSplittingAt(splitPoint, thread);
			int over = this.getOver(splitPoint, thread);
			int leftUnder = this.getUnder(thread, node, true);
			int rightUnder = this.getUnder(thread, node, false);
			int overM = ensureMinpos(minPos, over);
			int leftUnderM = ensureMinpos(minPos,leftUnder);
			int rightUnderM = ensureMinpos(minPos, rightUnder);
			int select = Math.min(overM, Math.min(leftUnderM, rightUnderM));
			if(select == over)
			{sampleOver(splitPoint,ba,annealing,thread,tua);}
			else if(select == leftUnder)
			{sampleLeftUnder(splitPoint,ba,annealing,thread,tua);}
			else if (select == rightUnder) 
			{sampleRightUnder(splitPoint,ba,annealing,thread,tua);}
			else
			{break;}
			minPos = select;
		}
	}
	/**
	 * @param splitPoint
	 * @param thread
	 * @return
	 */
	protected int getOver(int splitPoint, int thread)
	{
		int node = this.getNodeSplittingAt(splitPoint, thread);
		if(node == this.getRootPosition())
		{return -1;}
		return this.getMiddle(this.getAbove(node, thread), thread);
	}
	/**
	 * @param thread
	 * @param node
	 * @param left
	 * @return
	 */
	protected int getUnder(int thread, int node, boolean left)
	{
		int leftUnder = this.getDominated(node, left, thread);
		leftUnder = leftUnder < this.getLength() ? -1 : this.getMiddle(leftUnder, thread);
		return leftUnder;
	}
	/**
	 * 
	 */
	private final double[] probs;
	/**
	 * 
	 * @param splitPoint
	 * @param ba
	 * @param annealing
	 * @param thread
	 * @param tua
	 */
	private void sampleRightUnder(int splitPoint, PopulationProbabilityAssigner ba,
			double annealing, int thread, ThreadedUniformAccess tua)
	{
		if(this.samplesHead())
		{
			int node = this.getNodeSplittingAt(splitPoint, thread);
			int s1 = splitPoint;
			int s2 = this.getMiddle(this.getDominated(node, false, thread), thread);
			double max = Double.NEGATIVE_INFINITY;
			max = allHeadAssignments(ba, annealing, thread, s1, s2, 0, max);
			this.flipLeft(node, thread);
			max = allHeadAssignments(ba, annealing, thread, s1, s2, 4, max);
			this.flipRight(node, thread);
			double sum = makeSum(max);
			int choice = select(thread, tua, sum);
			if(choice < 0)
			{throw new IllegalStateException("probabilities did not sum to one.");}
			if(choice/4==1)
			{this.flipLeft(node, thread);}
			makeHeadSettings(thread, s1, s2, choice);
		}
		else
		{
			int node = this.getNodeSplittingAt(splitPoint, thread);
			this.probs[0] = ba.makeLogProb(this, thread)*annealing;
			this.flipLeft(node, thread);
			this.probs[1] = ba.makeLogProb(this, thread)*annealing;
			this.flipRight(node, thread);
			double border = handleProbs(thread, tua);
			if(border > 0.0)
			{this.flipLeft(node, thread);}
		}
	}
	/**
	 * @param max
	 * @return
	 */
	private double makeSum(double max)
	{
		double sum = 0.0;
		for(int i=0;i<this.probs.length;++i)
		{sum += this.probs[i] = Math.exp(this.probs[i]-max);}
		return sum;
	}
	/**
	 * @param thread
	 * @param tua
	 * @param sum
	 * @return
	 */
	private int select(int thread, ThreadedUniformAccess tua, double sum)
	{
		int choice = -1;
		double border = tua.nextDouble(thread);
		for(int i=0;i<this.probs.length;++i)
		{
			border -= this.probs[i]/sum;
			if(border <= 0.0)
			{
				choice = i;
				break;
			}
		}
		return choice;
	}
	/**
	 * @param thread
	 * @param s1
	 * @param s2
	 * @param choice
	 */
	private void makeHeadSettings(int thread, int s1, int s2, int choice)
	{
		choice %= 4;
		switch(choice)
		{
		case 0:
			this.setHeadDirection(s1, true, thread);
			this.setHeadDirection(s2, true, thread);
			break;
		case 1:
			this.setHeadDirection(s1, true, thread);
			this.setHeadDirection(s2, false, thread);
			break;
		case 2:
			this.setHeadDirection(s1, false, thread);
			this.setHeadDirection(s2, true, thread);
			break;
		case 3:
			this.setHeadDirection(s1, false, thread);
			this.setHeadDirection(s2, false, thread);
			break;
		default:
			throw new IllegalStateException("Illegal choice made");
		}
	}
	/**
	 * @param ba
	 * @param annealing
	 * @param thread
	 * @param s1
	 * @param s2
	 * @param k
	 * @param max 
	 */
	private double allHeadAssignments(PopulationProbabilityAssigner ba, double annealing,
			int thread, int s1, int s2, int k, double max)
	{
		this.setHeadDirection(s1, true, thread);
		this.setHeadDirection(s2, true, thread);
		max = Math.max(this.probs[k++] = ba.makeLogProb(this, thread)*annealing, max);
		this.setHeadDirection(s2, false, thread);
		max = Math.max(this.probs[k++] = ba.makeLogProb(this, thread)*annealing, max);
		this.setHeadDirection(s1, false, thread);
		this.setHeadDirection(s2, true, thread);
		max = Math.max(this.probs[k++] = ba.makeLogProb(this, thread)*annealing, max);
		this.setHeadDirection(s2, false, thread);
		max = Math.max(this.probs[k++] = ba.makeLogProb(this, thread)*annealing, max);
		return max;
	}
	/**
	 * @param thread
	 * @param tua
	 * @return
	 */
	protected double handleProbs(int thread, ThreadedUniformAccess tua)
	{
		double max = Math.max(this.probs[0], this.probs[1]);
		double sum = probs[0] = Math.exp(this.probs[0]-max);
		sum += probs[1] = Math.exp(this.probs[1]-max);
		double border = tua.nextDouble(thread);
		border -= probs[0]/sum;
		return border;
	}
	/**
	 * 
	 * @param splitPoint
	 * @param ba
	 * @param annealing
	 * @param thread
	 * @param tua
	 */
	private void sampleLeftUnder(int splitPoint, PopulationProbabilityAssigner ba,
			double annealing, int thread, ThreadedUniformAccess tua)
	{
		if(this.samplesHead())
		{
			int node = this.getNodeSplittingAt(splitPoint, thread);
			int s1 = splitPoint;
			int s2 = this.getMiddle(this.getDominated(node, true, thread), thread);
			double max = Double.NEGATIVE_INFINITY;
			max = allHeadAssignments(ba, annealing, thread, s1, s2, 0, max);
			this.flipRight(node, thread);
			max = allHeadAssignments(ba, annealing, thread, s1, s2, 4, max);
			this.flipLeft(node, thread);
			double sum = makeSum(max);
			int choice = select(thread, tua, sum);
			if(choice < 0)
			{throw new IllegalStateException("probabilities did not sum to one.");}
			if(choice/4==1)
			{this.flipRight(node, thread);}
			makeHeadSettings(thread, s1, s2, choice);
		}
		else
		{
			int node = this.getNodeSplittingAt(splitPoint, thread);
			this.probs[0] = ba.makeLogProb(this, thread)*annealing;
			this.flipRight(node, thread);
			this.probs[1] = ba.makeLogProb(this, thread)*annealing;
			this.flipLeft(node, thread);
			double border = handleProbs(thread, tua);
			if(border > 0.0)
			{this.flipRight(node, thread);}
		}
	}
	/**
	 * 
	 * @param splitPoint
	 * @param ba
	 * @param annealing
	 * @param thread
	 * @param tua
	 */
	private void sampleOver(int splitPoint, PopulationProbabilityAssigner ba,
			double annealing, int thread, ThreadedUniformAccess tua)
	{
		int node = this.getNodeSplittingAt(splitPoint, thread);
		int above = this.getAbove(node, thread);
		if(node == this.getDominated(above, true, thread))
		{this.sampleLeftUnder(this.getMiddle(above, thread), ba, annealing, thread, tua);}
		else
		{this.sampleRightUnder(this.getMiddle(above, thread), ba, annealing, thread, tua);}
	}
	/**
	 * @param minPos
	 * @param over
	 * @return
	 */
	protected int ensureMinpos(int minPos, int proposedPos)
	{return proposedPos >= minPos ? proposedPos : Integer.MAX_VALUE;}
}